Programming the AT Real-Time Clock Chip A Ferrari in Second Gear A couple of years ago, one of my high-flying friends mortgaged his life and bought a Ferrari--darned nice looking car and faster than anything I'd ever driven. The doggone thing was a helluva lot more fun than my Rabbit. We took it out on the back roads, ran it up past 130 and talked about it for months. Exhilarating. Funny thing though, he didn't normally get places faster than I did. In fact, many times it took him longer--usually because he was driving at the speed limit. (Small-town cops get their jollies stopping fancy cars). After a year or so he sold the Ferrari and bought something more sensible--said there was no reason to have a Ferrari when it never got out of second gear. Inside every At-compatible computer is a Motorola MC146818 Real-Time Clock (RTC) chip or one of its functional equivalents. (The earlier PC/XT class of machines lacks this feature.) This particular IC provides a real-time clock, 14 bytes of clock, calendar and control registers along with 50 bytes of general- purpose RAM all backed up by a battery that allows the information to be retained when the system is shut down. In addition, the chip is capable of generating a hardware interrupt at a program-specified frequency or time. In the AT, the primary function of the RTC is to retain time, date and system setup information when the system is shut down. At power-on, the BIOS power on self-test code verifies the system setup information and sets the system clock from the RTC's date and time. The chip is then completely ignored unless specifically activated by an application program. There are BIOS functions that can access the RTC, but they are quite limited. With the exception of the functions that read and set the RTC's clock, I've found the BIOS functions of limited use, and of questionable value due to their buggy implementation in many manufacturers' BIOS. The BIOS functions truly leave the RTC chip in second gear. To access the chip's full power, you must program it at the hardware level. CMOS RAM The CMOS RAM is divided into 3 areas: the clock/calendar bytes, the control registers, and general-purpose RAM. Table 1 shows the CMOS RAM locations and what each is used for. As you can see, there are 33 bytes--more than half of the total--of "reserved" memory in three areas. These locations are not currently defined by the AT BIOS and may be used to store information that will be retained after the power is shut down. The four status registers (A through D), located appropriately at CMOS locations 0AH through 0DH, define the chip's operating parameters and provide information about interrupts and the state of the RTC. See Figure 1 for information on the particulars of each status register. With very few restrictions, all CMOS RAM locations may be directly accessed by an application program. Locations 11H, 13H, and 1BH through 2DH are used in calculating the CMOS checksum that the BIOS stores at locations 2FH and 2EH. If a program changes these bytes it must also re-calculate the checksum and store the new value. Changing these bytes without replacing the checksum result in a "CMOS Checksum Error" at boot--forcing you to run Setup before you can access the hard disk. The reserved memory at locations 34H through 3FH is not used in checksum calculations and may therefore be changed with apparent impunity. I say "apparent" only because individual hardware or BIOS manufacturers may use the reserved CMOS RAM locations for extended system setup information. The Update Cycle Once each second, for 1,948us, the Time, Calendar, and Alarm (TCA) bytes are switched to the RTC's update logic to be advanced by one second and to check for an alarm condition. At completion, the RTC sets the UF bit in Status Register C to indicate that an update cycle has completed. An interrupt will not be asserted unless UIE in Status Register B is also set. During the update cycle, the TCA bytes are unavailable to the outside world and the result of any attempted access is unpredictable. The trick is knowing when the TCA bytes are available. Enter UIP--the Update In Progress flag (bit 7 of status register A). 244us before the start of an update cycle (and before switching the TCA bytes to the internal update logic), the RTC logic sets the UIP flag in Status Register A. The flag is then reset at the completion of the update cycle (2,228us later). The UIP flag is set whenever the TCA bytes are unavailable. In addition, when the UIP flag is cleared, the program is guaranteed at least 244us (1,464 machine cycles at 6MHz) to perform an access--plenty of time, even with subroutine call overhead. In order to prevent an update from occurring while setting the time or date, it is possible to halt the update cycle by setting bit 7 (the SET bit) in Status Register B. Once the program has initialized the TCA bytes, the SET bit is cleared and the update cycle continues. Hardware Interrupts The RTC is capable of generating three different types of interrupts. The periodic interrupt can be programmed to occur at frequencies ranging from 2 Hz to 8.192 KHz. The update-ended interrupt, if enabled, will occur once each second, after the update cycle has completed. And the alarm interrupt can be programmed to occur at any specified time. Status Register B defines which events within the RTC can generate an interrupt, but the AT's 2nd Programmable Interrupt Controller (PIC) determines whether or not the RTC can interrupt the processor. In order for RTC-generated interrupts to be recognized, bit 0 of the PIC's mask register must be set to 0. Before this interrupt is enabled, you must have a working INT 70H ISR installed and the RTC should be programmed to function as you require. All three interrupts (periodic, alarm and update-ended) are asserted on a single pin and cause the INT 70H ISR to be executed. It is the ISR's responsibility to identify which interrupts have occurred and to dispatch to the proper routine. Determining which interrupts have occurred is a simple matter of reading Status Register C and "AND-ing" with Status Register B. Bits that remain set reflect which events caused the hardware interrupt. Regular as Clockwork Setting the UIE bit in Status Register B enables the once-per- second update ended interrupt that occurs at the end of the update cycle. The ISR that processes this interrupt must complete within one second. One use of this interrupt is to keep the current RTC time in RAM so an applications program doesn't have to wait for an update cycle before reading CMOS RAM. Each time the update-ended interrupt occurs, the ISR reads the RTC time and date and stores it in memory. Any program that requires the time can then read it directly from memory rather than going to CMOS RAM and possibly having to wait for an entire update cycle. Periodically Speaking The RTC can be programmed to generate a periodic interrupt at several different rates, as shown in Table 2. When the PIE bit is set in Status Register B, the RTC will generate an interrupt at the frequency defined by the RS bits in Status Register A. The ISR that processes this interrupt must complete before the next periodic interrupt occurs. Accessing the TCA bytes during a periodic interrupt ISR is not recommended and, at higher interrupt frequencies, can cause strange behavior due to interrupts occurring while waiting for an update cycle to complete. There's one born every... The most versatile of the interrupts provided by the RTC is the alarm interrupt. In it's most basic form, the RTC is programmed to generate an interrupt once per day at the hour, minute and second specified by the alarm time in CMOS RAM. However, Motorola provided for "don't care" conditions that allow the alarm interrupt to be generated on a more frequent and/or irregular basis. Setting any of the alarm bytes to FFH creates a "don't care" condition for that particular byte, which means it will match ANY value. So, for example, an alarm time of FF:00:00 will generate an interrupt once each hour, on the hour. Similarly, FF:FF:00 generates an interrupt once each minute, and FF:FF:FF causes an interrupt to occur once each second. Odd combinations of "don't care" conditions can create some strange and possibly quite useful interrupt frequencies. For example, an alarm time of FF:00:FF will generate an interrupt once per second during the first minute of each hour, and 00:FF:00 will generate an interrupt once each minute for the first hour of the day. Using the RTC Functions RTC.H (Listing 1) contains function prototypes and defined constants used in accessing the functions and RTCHDW.C (Listing 2) contains the functions that access the RTC. CMOS.C (Listing 3) is a small program that dumps the contents of CMOS RAM to the screen and adds 1 to the value at location 1BH. The CMOS checksum is then re-calculated and the program exits. If you turn your computer off, re-boot and run the program again, you will see that the value you stored at location 1BH is still there. HDWTST.C (Listing 4) sets up a simple clock and a 4,096 ticks-per-second counter that displays the number of ticks once each second. Pressing any key will exit the program. Timing Considerations Using the precision Zen timer from Michael Abrash's excellent book Zen of Assembly Language, I timed the NewRTCint() routine. On my 10MHz Kaypro AT, the routine takes approximately 110us, or about 1,100 machine cycles, to execute when all of the interrupt routines are called. Using this information, we can calculate the maximum supportable interrupt rate for a given 80286 processor by dividing the clock rate by 1,100. For example, a 12Mhz 80286 can support a maximum interrupt frequency of 10.909 KHz (12,000,000 / 1,100). Be aware, however, that this is the absolute maximum supportable frequency. To paraphrase the auto ads, your actual rate will probably be less. Why? Because this calculation does not take into account other work that the processor will probably be doing. It assumes that the processor is doing nothing but servicing the RTC interrupts. NewRTCint(), could be sped up a great deal by replacing the calls to ReadCMOS() with in-line assembly language code. The two calls to ReadCMOS() take a total of approximately 54us--almost one-half of the total execution time. Further speed gains can be realized by replacing the two calls to outportb() with in-line assembly language. I've elected not to make these modifications in order to more clearly illustrate the concepts without muddying up the waters with efficiency considerations. Other Considerations You'll notice that quite a lot of the code for NewRTCint() deals with storing the caller's context and preventing stack overflow. This is required because the RTC will sometimes generate a periodic interrupt asynchronously--while the alarm interrupt is being processed. Without this stack check logic, NewRTCint() is not re-entrant and the asynchronous interrupt will cause a system crash. The only other way to handle this potential problem is to keep interrupts off for the duration of the routine--possibly losing other interrupts in the process. RTCHDW.C must be compiled with standard stack frames enabled (-k switch with Borland C++). If you compile without standard stack frames, the NewRTCInt() routine will not exit properly. If, for some reason, you want to compile without standard stack frames, replace the pop bp instruction in NewRTCInt() (right before the iret instruction) with a line that reads: asm mov bp,[word ptr cs:bp+6] As with any other device, it is possible that another program is using the RTC when your program wants to. The easiest way to tell if another program is using the RTC is to examine bit 0 of the 2nd PIC's mask register. If this bit is 0, then some other program is (or, in the case of one buggy BIOS that I encountered, was) using the RTC chip. If this bit is 0, examining the interrupt-enable flags in Status Register B will determine which (if any) RTC interrupts are being allowed. If none of the interrupt enable flags is set, then RTC interrupts are not being processed and your program can take control of the RTC without causing any problems.